#include "Sprite.h"

#include "lib3d/material/TextureFrame.h"
#include <math/utl.h>
#include <xml/Saver.h>
#include <logs/Log.h>

#include <boost/bind.hpp>

namespace liba {
namespace lib3d {
namespace node {

namespace {
	template<class T>
	const T& Stub() { static T v; return v; }
}

const Atom Sprite::node_name("Sprite");
const Atom Sprite::transparency_name("Transparency");
const Atom Sprite::frame_index_name("FrameIndex");
const Atom Sprite::src_name("Src");
const Atom Sprite::frame_name("Frame");
const Atom Sprite::color_name("Color");


Sprite::Sprite(const Atom& name, Node* parent)
	: Node2D(name, parent)
	, recreateVb(true)
	, transparency(0)
	, color(Color(1, 1, 1))
	, anchorPoint(0.5f, 0.5f)
	, fragment(
		std::numeric_limits<float>::quiet_NaN(),
		std::numeric_limits<float>::quiet_NaN(),
		std::numeric_limits<float>::quiet_NaN(),
		std::numeric_limits<float>::quiet_NaN())
	, absoluteFragment(false)
	, tile(1.0f, 1.0f)
	, wrap(false)
	, filterTexture(false)
	, frameIndex(Stub<double>, boost::bind(&Sprite::SetFrameIndex, this, _1))
	, source(Stub<filesystem::String>, boost::bind(&Sprite::SetSource, this, _1))
	, frame(Stub<std::wstring>, boost::bind(&Sprite::SetFrame, this, _1))
{
	node_type = node_name;

	hard::Manager::add_dependent(this);

	this->RegisterProperty(transparency_name, &this->transparency);
	this->RegisterProperty(frame_index_name, &this->frameIndex);
	this->RegisterProperty(src_name, &this->source);
	this->RegisterProperty(frame_name, &this->frame);
	this->RegisterProperty(color_name, &this->color);
}

Sprite::~Sprite()
{
	hard::Manager::remove_dependent(this);
}

Node* Sprite::Clone()
{
	return new Sprite(*this);
}

Sprite::Sprite(const Sprite &rhs)
	: Node2D(rhs)
	, recreateVb(true)
	, transparency(rhs.transparency)
	, color(rhs.color)
	, anchorPoint(rhs.anchorPoint)
	, fragment(rhs.fragment)
	, absoluteFragment(rhs.absoluteFragment)
	, tile(rhs.tile)
	, tileOffset(rhs.tileOffset)
	, texFileName(rhs.texFileName)
	, wrap(rhs.wrap)
	, filterTexture(rhs.filterTexture)
	, atlasInfo(rhs.atlasInfo)
	, frameIndex(Stub<double>, boost::bind(&Sprite::SetFrameIndex, this, _1))
	, source(Stub<filesystem::String>, boost::bind(&Sprite::SetSource, this, _1))
	, frame(Stub<std::wstring>, boost::bind(&Sprite::SetFrame, this, _1))
{
	node_type = node_name;

	hard::Manager::add_dependent(this);

	if(rhs.texture.get())
		texture.reset(new material::TextureFrame(*rhs.texture));
	if(rhs.oldTexture.get())
		texture.reset(new material::TextureFrame(*rhs.oldTexture));

	this->RegisterProperty(transparency_name, &this->transparency);
	this->RegisterProperty(frame_index_name, &this->frameIndex);
	this->RegisterProperty(src_name, &this->source);
	this->RegisterProperty(frame_name, &this->frame);
	this->RegisterProperty(color_name, &this->color);
}

filesystem::String Sprite::GetTextureName() const
{
	return texFileName;
}

filesystem::String Sprite::GetAtlasTextureName() const
{
	return atlasInfo.atlasName;
}

std::string Sprite::GetAtlasTextureFrame() const
{
	return atlasInfo.frameName;
}

void Sprite::on_add_hardware(hard::Manager::hardware_type* ha)
{
	hard_vector.push_back(SweetPair(ha, ha->create_vertex_buffer()));
}

void Sprite::on_save_attributes(xml::SaverBase * saver) const
{
	Node::on_save_attributes(saver);

	if(transparency > 0.0001)
		saver->save_attribute("Transparency", static_cast<double>(transparency));

	if(color != Color(1, 1, 1))
		saver->save_attribute("Color", static_cast<Color>(color));

	if(anchorPoint != math::Vector2<float>(0.5f, 0.5f))
		saver->save_attribute("Anchor", anchorPoint);

	if(tile != math::Vector2<float>(1.0f, 1.0f))
		saver->save_attribute("Tile", tile);

	if(tileOffset != math::Vector2<float>(0.0f, 0.0f))
		saver->save_attribute("TileOffset", tileOffset);

	if(!math::is_nan(fragment.x))
		saver->save_attribute("Fragment", fragment);

	if(absoluteFragment)
		saver->save_attribute("AbsoluteFragment", absoluteFragment);

	if(wrap)
		saver->save_attribute("AbsoluteFragment", wrap);

	if(!atlasInfo.atlasName.empty())
	{
		saver->save_attribute("Atlas", atlasInfo.atlasName);
		saver->save_attribute("Frame", atlasInfo.frameName);
	}
	else if(!texFileName.empty())
	{
		saver->save_attribute("Src", texFileName);
	}
}

bool Sprite::on_attribute(xml::Provider * prov, const std::string & name, const std::string & value)
{
	if(name == "Transparency")
	{
		double transparency;
		bool res = converter::convert(value, &transparency);
		if (res)
			this->transparency = transparency;
		return res;
	}
	if(name == "Color")
	{
		Color color;
		bool res = converter::convert(value, &color);
		if (res)
			this->color = color;
		return res;
	}
	if(name == "Anchor")
		return converter::convert(value, &anchorPoint);
	if(name == "Tile")
		return converter::convert(value, &tile);
	if(name == "TileOffset")
		return converter::convert(value, &tileOffset);
	if(name == "Fragment")
		return converter::convert(value, &fragment);
	if(name == "AbsoluteFragment")
		return converter::convert(value, &absoluteFragment);
	if(name == "Wrap")
		return converter::convert(value, &wrap);
	if(name == "Src")
	{
		filesystem::String source;
		bool res = converter::convert(value, &source);
		if (res)
			this->source.Set(source);
		return res;
	}
	if(name == "Atlas")
		return converter::convert(value, &atlasInfo.atlasName);
	if(name == "Frame")
		return converter::convert(value, &atlasInfo.frameName);
	
	return Node::on_attribute(prov, name, value);
}

void Sprite::SetAnchorPoint(const math::Vector2 <float> & point)
{
	anchorPoint = point;
	recreateVb  = true;
}

bool Sprite::on_end_node(xml::Provider * prov)
{
	if(!atlasInfo.atlasName.empty())
		texture.reset(new material::TextureFrame(atlasInfo.atlasName, Atom(atlasInfo.frameName)));
	else
		if(!texFileName.empty())
			texture.reset(new material::TextureFrame(texFileName, wrap));

	return true;
}

void Sprite::RenderToList(hard::Hardware * hardware, hard::RenderList * render_list)
{
	hard::VertexBuffer* vb = find_data(hardware);

	if(recreateVb && texture.get() && texture->IsReady())
	{
		// TODO: Make preload sizes!!!
		try
		{
			fill_vb(vb);
		}
		catch(const std::overflow_error &e)
		{
			LOG_WARN("Sprite", "Exception: %s!", e.what());
			return;
		}

		oldTexture.reset();
		recreateVb = false;
	}

	if((texture.get() && texture->IsReady()) || (oldTexture.get() && oldTexture->IsReady()))
	{
		hard::RenderModel render_model;
		render_model.filter_texture = filterTexture;
		render_model.world = GetGlobalMatrix();
		render_model.vb = vb;
		render_model.primitive_start = 0;
		render_model.primitive_count = 4;
		render_model.vertex_start = 0;
		render_model.vertex_count = 12;
		render_model.material.diffuse = color;
		render_model.material.emissive = color;
		transparency = math::clamp( static_cast<double>(transparency), 0.0001, 0.9999 );
		render_model.material.transparency = transparency;
		render_model.material.specular_power = 0;
		if (this->oldTexture.get())
			this->oldTexture->render_to_list( hardware, &render_model.material.emissive_texture );
		else
			this->texture->render_to_list( hardware, &render_model.material.emissive_texture );
		render_list->models.push_back(render_model);
	}

	// Call base
	Node::RenderToList(hardware, render_list);
}

void Sprite::SetFrameIndex(double value)
{
	double frameIndex;
	// BUG: convert double to double.
	converter::convert(value, &frameIndex);
	texture->SetAtlasFrameIndex((int)frameIndex);
	recreateVb = true;
}

void Sprite::SetSource(const filesystem::String &value)
{
	this->oldTexture = this->texture;
	this->texture.reset(new material::TextureFrame(value));
	recreateVb = true;

	texFileName = value.c_str();
}

void Sprite::SetFrame(const std::wstring &value)
{
	Atom frameName;
	converter::convert(value, &frameName);
	texture->SetAtlasFrameName(frameName);
	recreateVb = true;

	std::string tmp;
	converter::convert(value, &tmp);
	atlasInfo.frameName = tmp.c_str();
}

void Sprite::SetTextureFilter(bool state)
{
	filterTexture = state;
}

void Sprite::SetTransparency(float _transparency = 0.0001)
{
	this->transparency.Set(_transparency);
}

void Sprite::SetColor(liba::Color color)
{
	this->color.Set(color);
}

int Sprite::GetWidth()
{
	if(texture.get() && texture->IsReady())
		return texture->GetWidth();

	return 0;
}

int Sprite::GetHeight()
{
	if(texture.get() && texture->IsReady())
		return texture->GetHeight();

	return 0;
}

math::Rect<float> Sprite::GetLocalBounds()
{
	math::Vector2<float> left = this->GetLeftBottom();

	return math::Rect<float>(
		left.x,
		left.y,
		static_cast<float>(this->GetWidth()),
		static_cast<float>(this->GetHeight()));
}

void Sprite::SetTexture(const filesystem::String &fileName)
{
	texture.reset(new material::TextureFrame(fileName));
	recreateVb = true;

	texFileName = fileName;
}
	
void Sprite::SetTexture(hard::Hardware * hardware, hard::Texture * texture)
{
	this->texture.reset(new material::TextureFrame(hardware, texture));
	recreateVb = true;
}

void Sprite::SetAtlasTexture(const filesystem::String &fileName, const std::string &frameName)
{
	texture.reset(new material::TextureFrame(fileName, Atom(frameName)));
	recreateVb = true;

	atlasInfo.atlasName = fileName;
	atlasInfo.frameName = frameName;
}

void Sprite::SetAtlasTexture(const filesystem::String &fileName, int frameIndex)
{
	texture.reset(new material::TextureFrame(fileName, frameIndex));
	recreateVb = true;
}

void Sprite::SetFragment(const math::Rect<float>& value)
{
	this->fragment = value;
	this->recreateVb = true;
}

math::Vector2 <float> Sprite::GetLeftBottom()
{
	return math::Vector2 <float> (-GetWidth() * anchorPoint.x, -GetHeight() * anchorPoint.y);
}

void Sprite::SetTile(const math::Vector2 <float> &tile)
{
	this->tile = tile;
	recreateVb = true;
}

void Sprite::SetTileOffset(const math::Vector2 <float> &offset)
{
	tileOffset = offset;
	recreateVb = true;
}

void Sprite::fill_vb(hard::VertexBuffer* vb)
{
	SimpleArray<float> positions(12*3);
	SimpleArray<float> uvs(12*2);
	float * pos_data = positions.data();
	float * uvs_data = uvs.data();

	float dx = GetWidth();
	float dy = GetHeight();
	float x = GetLeftBottom().x;
	float y = GetLeftBottom().y;
	float u = texture->TransformU(tileOffset.x);
	float v = texture->TransformV(tileOffset.y);
	float du = texture->TransformU(tile.x) - u;
	float dv = texture->TransformV(tile.y) - v;

	if (!math::is_nan(fragment.x))
	{
		if (absoluteFragment)
		{
			x += fragment.x;
			dx = fragment.width;
			u  = texture->TransformU(fragment.x / (float)GetWidth());
			du = texture->TransformU((fragment.x + fragment.width) / (float)GetWidth()) - u;
	
			y += fragment.y;
			dy = fragment.height;
			v = texture->TransformV(fragment.y / (float)GetHeight());
			dv = texture->TransformV((fragment.y + fragment.height) / (float)GetHeight()) - v;
		}
		else
		{
			x += fragment.x * GetWidth();
			dx = fragment.width * GetWidth();
			u  = texture->TransformU(fragment.x);
			du = texture->TransformU(fragment.x + fragment.width) - u;
	
			y += fragment.y * GetHeight();
			dy = fragment.height * GetHeight();
			v = texture->TransformV(fragment.y);
			dv = texture->TransformV(fragment.y + fragment.height) - v;
		}
	}

	//CCW
	*pos_data++ = x;
	*pos_data++ = y;
	*pos_data++ = 0;
	*uvs_data++ = u;
	*uvs_data++ = v;

	*pos_data++ = x;
	*pos_data++ = y + dy;
	*pos_data++ = 0;
	*uvs_data++ = u;
	*uvs_data++ = v + dv;

	*pos_data++ = x + dx;
	*pos_data++ = y;
	*pos_data++ = 0;
	*uvs_data++ = u + du;
	*uvs_data++ = v;

	*pos_data++ = x + dx;
	*pos_data++ = y;
	*pos_data++ = 0;
	*uvs_data++ = u + du;
	*uvs_data++ = v;

	*pos_data++ = x;
	*pos_data++ = y + dy;
	*pos_data++ = 0;
	*uvs_data++ = u;
	*uvs_data++ = v + dv;

	*pos_data++ = x + dx;
	*pos_data++ = y + dy;
	*pos_data++ = 0;
	*uvs_data++ = u + du;
	*uvs_data++ = v + dv;

	//CW
	*pos_data++ = x;
	*pos_data++ = y;
	*pos_data++ = 0;
	*uvs_data++ = u;
	*uvs_data++ = v;

	*pos_data++ = x + dx;
	*pos_data++ = y;
	*pos_data++ = 0;
	*uvs_data++ = u + du;
	*uvs_data++ = v;

	*pos_data++ = x;
	*pos_data++ = y + dy;
	*pos_data++ = 0;
	*uvs_data++ = u;
	*uvs_data++ = v + dv;

	*pos_data++ = x + dx;
	*pos_data++ = y;
	*pos_data++ = 0;
	*uvs_data++ = u + du;
	*uvs_data++ = v;

	*pos_data++ = x + dx;
	*pos_data++ = y + dy;
	*pos_data++ = 0;
	*uvs_data++ = u + du;
	*uvs_data++ = v + dv;

	*pos_data++ = x;
	*pos_data++ = y + dy;
	*pos_data++ = 0;
	*uvs_data++ = u;
	*uvs_data++ = v + dv;

	SimpleArray <float> empty_float;

	if(!vb->update(positions, empty_float, empty_float, uvs, empty_float))
	{
		THROW(Gluck(__FUNCTION__, "vb->update() failed"));
	}
}

void Sprite::CreateProxy(ProxyVisitor * proxyVisitor)
{
	proxyVisitor->CreateSpriteProxy(this);
}

} // namespace node
} // namespace lib3d
} // namespace liba